<?php
namespace aw;
use Exception;

class ReadAwdb
{
    private $fileSize;
    private $fileStream;
    private $indexBase;
    private $indexBaseByteSize;
    private $metadataLen;
    private $bytes;
    private $metaData;

    const _ARRAY_NUM = 1;
    const _POINTER_NUM = 2;
    const _STRING_NUM = 3;
    const _LONG_STRING_NUM = 4;
    const _UINT_NUM = 5;
    const _INT_NUM = 6;
    const _FLOAT_NUM = 7;
    const _DOUBLE_NUM = 8;

    public function __construct($database)
    {
        $this->fileStream = @fopen($database, 'rb');
        $this->fileSize = @filesize($database);

        $this->metadataLen = unpack('n', fread($this->fileStream, 2))[1];
        fseek($this->fileStream, 2);
        $metaData = json_decode(fread($this->fileStream, $this->metadataLen), true);

        $this->nodeCount = $metaData['node_count'];
        $this->ipVersion = $metaData['ip_version'];
        $this->decodeType = $metaData['decode_type'];
        $this->byteLen = $metaData['byte_len'];
        $this->columns = $metaData['columns'];
        $this->startLen = 2 + $this->metadataLen;
        $this->baseOffset = $this->nodeCount * $this->byteLen * 2 + $this->startLen;
    }

    //开始解析
    public function readAwdb($ipAddress)
    {
        $nodeIndex = $this->findIndex($ipAddress);
        if ($nodeIndex == "") {
            return json_decode('{}');
        }
        $pointer = $this->baseOffset + $nodeIndex - $this->nodeCount - 10;
        if ($pointer >= $this->fileSize) {
            throw new Exception("The aw DB file's search tree is corrupt!");
        }
        if ($this->decodeType ==1){
            $data =  $this->decodeContentStructure($pointer);
        }elseif ($this->decodeType ==2){
            $data =  $this->decodeContentDirect($pointer);
        }else{
            throw new Exception("The decodeType is Error!");
        }
        return $data;
    }

     public function decodeContentDirect($offset)
    {
        //开始解析
        $bytes = $this->openFile($this->fileStream, $offset, 4);
        $offset = $offset + 4;
        list(, $size) = unpack('I', strrev($bytes));
        $bytes = $this->openFile($this->fileStream, $offset, $size);
        $tempList = explode("\t", $bytes);
        $dic = array();
        foreach ($this->columns as $i => $value) {
            $dic[$value] = $tempList[$i];
        }
        return $dic;
    }

    public function decodeContentStructure($offset)
    {
        //开始解析
        list($data) = $this->decodeContent($offset);
        $data = $this->mapkeyValue($this->columns, $data);
        return $data;
    }

    public function openFile($stream, $offset, $numberOfBytes)
    {
        if ($numberOfBytes === 0) {
            return '';
        }
        if (fseek($stream, $offset) === 0) {
            $value = fread($stream, $numberOfBytes);
            if (ftell($stream) - $offset === $numberOfBytes) {
                return $value;
            }
        }
        throw new Exception('The aw DB file contains bad data');
    }

    private function bigEndianToInt($bytes)
    {
        $val = 0;
        $num = strlen($bytes);
        for ($i = 0; $i < $num; $i++) {
            $val |= ord($bytes[$i]) << (8 * ($num-1 - $i));
        }
    return $val;
    }

    private function isIpv4($ipAddress) {
        // 检查是否是合法的 IPv4 地址
        return filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
    }

    private function isIpv6($ipAddress) {
        // 检查是否是合法的 IPv4 地址
        return filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
    }

    private function intToIpv4($ip_address) {
        // 将整数转换为 IPv4 地址
        $ipv4_address = long2ip($ip_address);
        return $ipv4_address;
    }

    private function intToIpv6($ip_address) {
        // 1. 将十进制字符串 $ip_address 转换为 GMP 大数对象，并将其转换为 16 进制字符串
        $int_ip = gmp_strval(gmp_init(str_replace(",", "", number_format($ip_address)), 10), 16);
        // 将 $int_ip 生成32位的十六进制字符串，不足的部分左侧补 0
        $hex_ip = str_pad($int_ip, 32, '0', STR_PAD_LEFT);
        // 将 $hex_ip 按每4位拆分，并用冒号连接，形成 IPv6 地址格式
        $ipv6_address = implode(':', str_split($hex_ip, 4));
        // 输出IPv6地址
        return $ipv6_address;
    }

    private function ipv4MapIpv6($ip_address) {
        // 将 IPv4 地址转换为 IPv6 地址
        $ipv6_address = inet_pton("::ffff:$ip_address");
        // 将 IPv6 地址转换为字符串形式
        $ipv6_string = inet_ntop($ipv6_address);
        return $ipv6_string;
    }

    private function isIPv4MappedIPv6($ip) {
        $ipv6 = inet_pton($ip);
        if (strlen($ipv6) == 16) {
            $ipv4MappedIpv6 = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff";
            return (substr($ipv6, 0, strlen($ipv4MappedIpv6)) === $ipv4MappedIpv6);
        }
        return false;
    }

    private function findIndex($ipAddress)
    {
        if ($this->ipVersion == '4_6') {
            if (is_int($ipAddress)) {
                if ($ipAddress < pow(2, 32)) {
                    $ipAddress = $this->intToIpv4($ipAddress);
                    $ipAddress = $this->ipv4MapIpv6($ipAddress);
                }
            } elseif ($this->isIpv4($ipAddress)) {
                $ipAddress = $this->ipv4MapIpv6($ipAddress);
            }
        }
        if (is_int($ipAddress) || is_double($ipAddress) || (strpos($ipAddress, ":") === false and
                        strpos($ipAddress, ".") === false)) {
            if ($this->ipVersion == '4') {
                if ($ipAddress > 2**32) {
                    throw new Exception(sprintf("The database is IPv4 library, but you are using ipv6 queries! The IP is:%d", $ipAddress));
                }
                $ipAddress = $this->intToIpv4($ipAddress);
            } else {
                if ($ipAddress < 2**32) {
                    throw new Exception(sprintf("The database is IPv6 library, but you are using ipv4 queries! The IP is:%d", $ipAddress));
                }
                $ipAddress = $this->intToIpv6($ipAddress);
            }
        } else {
            if ($this->isIpv4($ipAddress)) {
                if ($this->ipVersion == '4') {
                    ;
                } else {
                    throw new Exception(sprintf("The database is IPv6 library, but you are using ipv4 queries! The IP is:%s", $ipAddress));
                }
            } elseif ($this->isIpv6($ipAddress)) {
                if ($this->ipVersion == '6' || $this->ipVersion == '4_6') {
                    ;
                } else {
                    throw new Exception(sprintf("The database is IPv4 library, but you are using ipv6 queries! The IP is:%s", $ipAddress));
                }
            } else {
                throw new Exception(sprintf("Invalid IP address %s!", $ipAddress));
            }
        }
        $nodeIndex = 0;

        if ($this->ipVersion == '4_6') {
            if ($this->isIPv4MappedIPv6($ipAddress)){
                $nodeIndex = 96;
            }
        }
        $rawAddress = unpack('C*', inet_pton($ipAddress));
        $bitCount = \count($rawAddress) * 8;
        $i = $nodeIndex;
        $nodeCount = $this->nodeCount;
        $byteLen = $this->byteLen;
        while ($i < $bitCount) {
            if ($nodeIndex >= $nodeCount){
                break;
            }
            $tempBit = 0xFF & $rawAddress[($i >> 3) + 1];
            $bitValue = 1 & ($tempBit >> 7 - ($i % 8));
            $bytes = $this->openFile($this->fileStream, $nodeIndex * 2 * $byteLen + $bitValue * $byteLen + $this->startLen, $byteLen);
            $nodeIndex = $this->bigEndianToInt($bytes);
            ++$i;
        }
        if ($nodeIndex == $this->nodeCount) {
            echo "Invalid nodeIndex in search tree\n";
            return "";
        }
        if ($nodeIndex > $this->nodeCount) {
            return $nodeIndex;
        }
        throw new Exception('Invalid nodeIndex in search tree');
    }


     public function decodeContent($offset)
    {
        //开始解析
        $dataType = \ord($this->openFile($this->fileStream, $offset, 1));
        ++$offset;

        switch ($dataType) {
            case self::_ARRAY_NUM:
                $size = \ord($this->openFile($this->fileStream, $offset, 1));
                ++$offset;
                $array = [];
                for ($i = 0; $i < $size; ++$i) {
                    list($value, $offset) = $this->decodeContent($offset);
                    array_push($array, $value);
                }
                return [$array, $offset];
        }
        switch ($dataType) {
            case self::_POINTER_NUM:
                list($pointer, $offset) = $this->getPointer($offset);
                list($result) = $this->decodeContent($pointer);
                return [$result, $offset];
            case self::_STRING_NUM:
                $size = \ord($this->openFile($this->fileStream, $offset, 1));
                ++$offset;
                $bytes = $this->openFile($this->fileStream, $offset, $size);
                $offset = $offset + $size;
                return [$bytes, $offset];
            case self::_LONG_STRING_NUM:
                $size = \ord($this->openFile($this->fileStream, $offset, 1));
                ++$offset;
                $bytes = $this->openFile($this->fileStream, $offset, $size);
                $offset = $offset + $size;
                $dataLen = $this->bigEndianToInt($bytes);
                $newBytes = $this->openFile($this->fileStream, $offset, $dataLen);
                $offset = $offset + $dataLen;
                return [$newBytes, $offset];
            case self::_UINT_NUM:
                $size = \ord($this->openFile($this->fileStream, $offset, 1));
                ++$offset;
                $bytes = $this->openFile($this->fileStream, $offset, $size);
                $offset = $offset + $size;
                $uintValue = $this->bigEndianToInt($bytes);
                return [$uintValue, $offset];
            case self::_INT_NUM:
                $bytes = $this->openFile($this->fileStream, $offset, 4);
                $offset = $offset + 4;
                list(, $intvalue) = unpack('l', strrev($bytes));
                return [$intvalue, $offset];
            case self::_FLOAT_NUM:
                $bytes = $this->openFile($this->fileStream, $offset, 4);
                $offset = $offset + 4;
                list(, $floatValue) = unpack('f', strrev($bytes));
                return [$floatValue, $offset];
            case self::_DOUBLE_NUM:
                $bytes = $this->openFile($this->fileStream, $offset, 8);
                $offset = $offset + 8;
                list(, $doubleValue) = unpack('d', strrev($bytes));
                return [$doubleValue, $offset];
            default:
                throw new Exception('Invalid data_type');
        }
    }

    private function getPointer($offset)
        {
        $size = \ord($this->openFile($this->fileStream, $offset, 1));
        ++$offset;
        $new_offset = $offset +$size;
        $bytes = $this->openFile($this->fileStream, $offset, $size);
        $buf = $this->bigEndianToInt($bytes);
        $pointer =$this->baseOffset + $buf;
        return [$pointer, $new_offset];
        }

    private function mapKeyValue($keys, $values) {
        if (count($keys) == count($values)) {
            $count = count($keys);
            $resultDict = array_combine(array_slice($keys, 0, $count), array_slice($values, 0, $count));
        } else {
            $count = count($values) - 1;
            $forepartDict = array_combine(array_slice($keys, 0, $count), array_slice($values, 0, $count));

            $keysList = $keys[count($keys) - 1];
            $valuesList = $values[count($values) - 1];
            $multiAreasName = $keys[count($keys) - 2];
            $endDict = [$multiAreasName => array_map(function($row) use ($keysList) {
                return array_combine($keysList, $row);
            }, $valuesList)];
            $resultDict = array_merge($forepartDict, $endDict);
        }
        return $resultDict;
    }

    public function close()
    {
        fclose($this->fileStream);
    }

}